/*
 * client.cpp
 *
 *  Created on: Jun 8, 2012
 *      Author: avihay
 */
#include "client.h"
#include "common_defs.h"
#include "../protocol/protocol.h"
#include <iostream>
#include <sys/socket.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <string>
#include <cstring>
#include <cstdlib>

using namespace std;

#define MAX_MSG_INPUT	(140)
#define WELCOME_MESSAGE	"Connected successfully"
#define AUTO_PROTOCOL	(0)
#define SPACE_CHAR		" "
#define EMPTY_STRING	""
#define AT_CHAR			"@"
#define NULL_TERM_LEN	1

Client::Client(const string &client_name, const string &server_address ,int port):
				_client_name(client_name),_server_add(server_address),_server_port(port)
{
	FD_ZERO( &_rd_fd_set );
	FD_ZERO( &_wr_fd_set );
	FD_ZERO( &_ex_fd_set );
	_is_client_on = false;
	_is_msg_completed = true;
	_bytes_left_to_read = 0;
}

result_t Client::start_client()
{

	int				ret_val;
	int				msg_len;
	result_t		res;
	fd_set			read_fd_set;
	fd_set			write_fd_set;
	fd_set			except_fd_set;
	string			in_cmd;
	string			in_arg;
	command_t		cmd_to_execute;
	in_msg_t		msg_from_server;

	DBG_PRINT("Opening a connection");

	// Open a new client connection to the server
	res = open_client_socket(_server_port, _server_add, _socket_fd);
	if( OPERATION_SUCCESS != res)
	{
		PRINT_ERROR("Connection to server failed.");
		return ERROR_NETWORK_CONN;
	}

	// Send welcome to server
	msg_len = _client_name.length();
	res = send_msg_to_server(msg_len + NULL_TERM_LEN, MESSAGE_TYPE_CONNECT, _client_name.c_str() );

	if (OPERATION_SUCCESS != res)
	{
		PRINT_ERROR("Connection to server failed.");
		return ERROR_NETWORK_CONN;
	}

	res = get_msg_from_server(msg_from_server);
	if(OPERATION_SUCCESS != res)
	{
		PRINT_ERROR("Connection to server failed.");
		return ERROR_NETWORK_CONN;
	}

	// Unblock the socket after we have a successful connection
	ret_val = fcntl ( _socket_fd, F_SETFL, O_NONBLOCK );
	if (-1 == ret_val)
	{
		ERR_PRINT("Connection to server failed.");
		return ERROR_SYSTEM_CALL;
	}


	if (msg_from_server.header.type == MESSAGE_TYPE_CONNECT_RESPONSE  && msg_from_server.header.status == RESPONSE_STATUS_SUCCESS)
	{
		PRINT_MSG(WELCOME_MESSAGE);
	}
	else
	{
		if(msg_from_server.header.type == MESSAGE_TYPE_CONNECT_RESPONSE)
		{
			/* We are in the given protocol, but the server decided
			* to decline our request. We print the msg from the server
			*/
			PRINT_ERROR("Client '" << _client_name <<"' is already in use." );
		}
		else
		{
			PRINT_ERROR("Unable to connect. Either the server is not responding or there is a protocol mismatch");
		}
		return ERROR_NETWORK_CONN;
	}

	// Add incoming/read file descriptors
	FD_SET(STDIN_FILENO, &_rd_fd_set);
	FD_SET(_socket_fd, &_rd_fd_set);

	_is_client_on = true;

	while(_is_client_on)
	{
		memcpy( & read_fd_set, & _rd_fd_set, sizeof(read_fd_set) );
		memcpy( & write_fd_set, & _wr_fd_set, sizeof(write_fd_set) );
		DBG_PRINT("Iterating ... Client is on");
		ret_val = select( max(_socket_fd, STDIN_FILENO) + 1 , &read_fd_set, & write_fd_set, &_ex_fd_set, NULL);
		if (-1 == ret_val)
		{
			PRINT_ERROR("Select operation failed failed");
			exit(1);
		}

		/*
		 * INPUT SOURCE: Standard input
		 */
		if ( FD_ISSET( STDIN_FILENO, & read_fd_set ) )
		{
			cmd_to_execute.cmd = COMMAND_UNKNOWN;
			cmd_to_execute.arg.clear();
			getline(cin, in_cmd);
			res = build_command_from_string(in_cmd, cmd_to_execute  );
			if( OPERATION_SUCCESS != res)
			{
				DBG_PRINT("Error: Invalid command.");
				continue;
			}

			DBG_PRINT("Command is: " << cmd_to_execute.cmd);
			DBG_PRINT("Arg is: " << cmd_to_execute.arg << " Length: " << cmd_to_execute.arg.length());

			switch(cmd_to_execute.cmd)
			{
			case COMMAND_FOLLOW: 				follow(cmd_to_execute); 			break;
			case COMMAND_UNFOLLOW: 				unfollow(cmd_to_execute); 			break;
			case COMMAND_WHO:					who(cmd_to_execute);				break;
			case COMMAND_TWIT:					twit(cmd_to_execute);				break;
			case COMMAND_DIRECT_MESSAGE:		direct_message(cmd_to_execute); 	break;
			case COMMAND_DISCONNECT:			exit_client(cmd_to_execute);		break;
			case COMMAND_PUBLIC_ANNOUNCEMENT: 	public_announce(cmd_to_execute);	break;
			default: 							PRINT_ERROR("Unkown Command");		break;
			}
		}

		/*
		 * INPUT SOURCE: Network
		 */
		if( FD_ISSET( _socket_fd, & read_fd_set) && _is_client_on )
		{
			res = get_msg_from_server(msg_from_server);

			if( res == ERROR_ZERO_SIZE_MESSAGE )
			{
				_is_client_on = false;
				PRINT_ERROR("Server terminated connection. Disconnecting.");
				continue;
			}

			if( res == ERROR_BAD_MESSAGE )
			{
				PRINT_ERROR("Bad message. Skipping and not proccesing.");
				continue;
			}

			if(_is_msg_completed)
				PRINT_MSG(msg_from_server.payload);

			// Remove extended memory for message payload
			delete [] msg_from_server.payload;
		}

		// Check if the server disconnected
		if ( FD_ISSET( _socket_fd, &except_fd_set ) )
		{
			cout << "Error: Server Disconnected. Closing client" << endl;
		}
	}

	return OPERATION_SUCCESS;
}

Client::~Client()
{
	sleep(1); // make sure the socket is flushed before closing the socket

	if ( shutdown( _socket_fd, SHUT_RDWR ) || close(_socket_fd) )
	{
		perror("Error: during closing client socket");
	}

	DBG_PRINT("DESTRUCTING THE CLIENT!");
}


// *****************************************************************
// ************* COMMAND IMPLEMENTATION ****************************
// *****************************************************************
result_t	Client::follow(const command_t &cmd)
{
	DBG_PRINT("FOLLOWING " << cmd.arg);
	send_msg_to_server(cmd.arg.length()+NULL_TERM_LEN, MESSAGE_TYPE_FOLLOW, cmd.arg.c_str() );
	return OPERATION_SUCCESS;
}

result_t	Client::unfollow(const command_t &cmd)
{
	DBG_PRINT("Unfollowing "  << cmd.arg);
	send_msg_to_server(cmd.arg.length()+NULL_TERM_LEN, MESSAGE_TYPE_UNFOLLOW, cmd.arg.c_str() );
	return OPERATION_SUCCESS;
}

result_t	Client::who(const command_t &cmd)
{
	send_msg_to_server(0, MESSAGE_TYPE_WHO, NULL);
	return OPERATION_SUCCESS;
}

result_t	Client::twit(const command_t &cmd)
{
	string tmp_str;

	tmp_str = cmd.arg;

	// Argument is message to be sent
	if(cmd.arg.length() > MAX_MSG_INPUT)
		tmp_str = tmp_str.substr(0, MAX_MSG_INPUT);

	DBG_PRINT("Twitting ...");
	send_msg_to_server(tmp_str.length()+NULL_TERM_LEN, MESSAGE_TYPE_TWIT, tmp_str.c_str());
	return OPERATION_SUCCESS;
}

result_t	Client::direct_message(const command_t &cmd)
{
	string	tmp_str;
	string 	tmp_username;
	string 	tmp_message;
	size_t	at_char_location;

	at_char_location = cmd.arg.find(AT_CHAR);
	if( at_char_location == string::npos || at_char_location == 0)
	{
		PRINT_ERROR("Invalid arguments for DM command");
		return ERROR_INVALID_ARGS;
	}

	tmp_username = cmd.arg.substr(0, at_char_location);
	tmp_message	 = cmd.arg.substr(at_char_location+1, cmd.arg.length());

	if(tmp_username.find(SPACE_CHAR) != string::npos || tmp_username.find(AT_CHAR) != string::npos)
		return ERROR_INVALID_ARGS;

	if(tmp_message.length() > MAX_MSG_INPUT)
		tmp_message = tmp_message.substr(0,MAX_MSG_INPUT);

	tmp_str = tmp_username + AT_CHAR + tmp_message;

	DBG_PRINT("Direct Messaging ... "  << cmd.arg);
	send_msg_to_server(tmp_str.length()+NULL_TERM_LEN, MESSAGE_TYPE_DIRECT_MESSAGE, tmp_str.c_str() );
	return OPERATION_SUCCESS;
}

result_t	Client::public_announce(const command_t &cmd)
{
	string tmp_str;

	tmp_str = cmd.arg;
	if(cmd.arg.length() > MAX_MSG_INPUT)
		tmp_str= cmd.arg.substr(0,MAX_MSG_INPUT);

	DBG_PRINT("Publicly announcing ... "  << cmd.arg);
	send_msg_to_server(tmp_str.length()+NULL_TERM_LEN, MESSAGE_TYPE_PUBLIC_ANNOUNCEMENT, tmp_str.c_str() );
	return OPERATION_SUCCESS;
}

result_t	Client::exit_client(const command_t &cmd)
{
	DBG_PRINT("Exiting ....");
	_is_client_on = false;
	send_msg_to_server(0, MESSAGE_TYPE_DISCONNECT, NULL );
	FD_ZERO( & _rd_fd_set ); // stop wait for incomes
	FD_ZERO( & _wr_fd_set ); // stop wait for incomes
	FD_ZERO( & _ex_fd_set ); // stop wait for incomes
	return OPERATION_SUCCESS;
}


// *****************************************************************
// ***************** AUXILARY FUNCTIONS ****************************
// *****************************************************************
result_t	Client::build_command_from_string(const std::string &input, command_t &cmd  )
{
	uint32_t		first_non_space;
	uint32_t		first_non_space_end;
	string			tmp_cmd;
	string			tmp_arg;

	tmp_cmd.clear();
	tmp_arg.clear();

	if(0 == input.length())
		return ERROR_INVALID_ARGS;

	// First non space will start the command
	first_non_space = input.find_first_not_of(SPACE_CHAR, 0);

	// First non space will find the end of the command
	first_non_space_end = input.find(SPACE_CHAR, first_non_space);

	DBG_PRINT(first_non_space);

	DBG_PRINT(first_non_space_end);

	if(first_non_space == string::npos)
		return ERROR_INVALID_ARGS;

	// put the command
	if(first_non_space_end != string::npos)
		tmp_cmd = input.substr(first_non_space, first_non_space_end );
	else // Argument less command
		tmp_cmd = input.substr(first_non_space, input.length() );

	// put the argument while trimming spaces
	if(first_non_space_end != input.length())
	{
		if ( input.find_first_not_of(SPACE_CHAR,first_non_space_end+1) == string::npos )
			tmp_arg.clear();
		else
			tmp_arg = input.substr(input.find_first_not_of(SPACE_CHAR,first_non_space_end+1), input.length());
	}

	cmd.arg = tmp_arg;

	if(tmp_cmd == "FOLLOW")
		cmd.cmd = COMMAND_FOLLOW;
	else if(tmp_cmd == "UNFOLLOW")
		cmd.cmd = COMMAND_UNFOLLOW;
	else if(tmp_cmd == "WHO")
		cmd.cmd = COMMAND_WHO;
	else if(tmp_cmd == "TWIT")
		cmd.cmd = COMMAND_TWIT;
	else if(tmp_cmd == "DM")
		cmd.cmd = COMMAND_DIRECT_MESSAGE;
	else if(tmp_cmd == "PA")
			cmd.cmd = COMMAND_PUBLIC_ANNOUNCEMENT;
	else if(tmp_cmd == "EXIT")
		cmd.cmd = COMMAND_DISCONNECT;
	else
		cmd.cmd = COMMAND_UNKNOWN;
	return OPERATION_SUCCESS;
}

result_t Client::send_msg_to_server(int msg_length,messages_type_t type, const char* msg_data)
{
	out_msg_t		*msg_to_server;

	msg_to_server = (out_msg_t*)malloc(MSG_METADATA_LENGTH + msg_length);
	msg_to_server->header.type = (messages_type_t)htonl(type);
	msg_to_server->header.payload_length = htonl(msg_length);
	if(msg_length>0)
		memcpy( & msg_to_server->payload, msg_data, msg_length);

	int	bytecount;

	bytecount = send(_socket_fd, msg_to_server,  MSG_METADATA_LENGTH + msg_length , 0);
	if(-1 == bytecount)
	{
		ERR_PRINT("Cannot send message over the network");
		return ERROR_IO;
	}
	else
	{
		DBG_PRINT("Sent " << bytecount << " to the server");
		DBG_PRINT("Purging message of type " << (messages_type_t)(ntohl(msg_to_server->header.type)) );
		delete msg_to_server;
		return OPERATION_SUCCESS;
	}
}

result_t Client::get_msg_from_server(in_msg_t &msg_from_server)
{
	in_msg_t		tmp_msg;
	int 			bytecount;
	uint32_t		offset;

	DBG_PRINT("Invoking get msg from server");

	if(_is_msg_completed) // Read header only if we completed reading the last message
	{
		// Read message header
		bytecount = recv(_socket_fd, &tmp_msg.header,  MSG_METADATA_LENGTH , 0);
		DBG_PRINT("Recieved data header");

		if(0 == bytecount)
			return ERROR_ZERO_SIZE_MESSAGE;

		if(bytecount != MSG_METADATA_LENGTH)
			return ERROR_BAD_MESSAGE;

		msg_from_server.header.type = (messages_type_t)ntohl(tmp_msg.header.type);
		msg_from_server.header.status = (response_status_t)ntohl(tmp_msg.header.status);
		msg_from_server.header.payload_length = ntohl(tmp_msg.header.payload_length);

		if(0 == msg_from_server.header.payload_length)
			return OPERATION_SUCCESS;

		msg_from_server.payload = new char[msg_from_server.header.payload_length];
		_bytes_left_to_read = msg_from_server.header.payload_length;
	}

	// Start reading the payload
	offset = msg_from_server.header.payload_length - _bytes_left_to_read;
	DBG_PRINT("trying to read payload of length " <<  msg_from_server.header.payload_length);
	bytecount = recv(_socket_fd, msg_from_server.payload + offset,  msg_from_server.header.payload_length , 0);
	DBG_PRINT("Finished reading payload of length " << bytecount);

	_bytes_left_to_read -=  bytecount;
	_is_msg_completed = (0 == _bytes_left_to_read) ? true : false;

	return OPERATION_SUCCESS;

}

result_t	Client::open_client_socket(int server_port, string server_address, int &socket_fd)
{
	int 			ret_val;
	sockaddr_in		srvr_socket_addr;

	// Prepare server socket
	srvr_socket_addr.sin_family = AF_INET;
	srvr_socket_addr.sin_port = htons(server_port);
	ret_val = inet_aton(server_address.c_str(), &(srvr_socket_addr.sin_addr) );
	if ( ! ret_val )
	{
		ERR_PRINT("Invalid server name");
		return ERROR_SYSTEM_CALL;
	}


	memset(&srvr_socket_addr.sin_zero,0,8);

	// Open an IPv4 socket and try perform a connection to the server.
	socket_fd = socket(AF_INET,SOCK_STREAM, AUTO_PROTOCOL);
	if( -1 == socket_fd)
	{
		ERR_PRINT("Failed to open a socket");
		return ERROR_SYSTEM_CALL;
	}

	int yes = 1;
	ret_val = setsockopt ( _socket_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int) );
	if( -1 == ret_val)
	{
		ERR_PRINT("Cannot set socket to reuse.");
		return ERROR_SYSTEM_CALL;
	}

	// Bind to the requested port
	ret_val = connect(socket_fd, ((struct sockaddr*) &srvr_socket_addr), sizeof(sockaddr));
	if( -1 == ret_val)
	{
		ERR_PRINT("Cannot bind to port.");
		return ERROR_SYSTEM_CALL;
	}
	DBG_PRINT("Binded to port");

	return OPERATION_SUCCESS;
}
